![]() |
![]() |
|
Nehmen wir an, dass ein Thread seine eigene Priorität mit der Eigenschaft Priority erhöhen soll, müssten Sie
codieren, damit der Thread auf sich selbst zugreifen kann. Auf die Eigenschaft Priority kommen wir später zu sprechen. Einen Thread für eine bestimmte Zeitdauer anhaltenIm Beispiel oben wurde eine Schleife eingebaut, um eine kleine Zeitverzögerung zu erreichen. Ohne Schleife könnte es – abhängig von der Taktfrequenz des Prozessors – sein, dass die gesamte Schleife des ersten Threads bereits vollständig abgearbeitet ist, bevor der zweite Thread zum ersten Mal in seine eigene Schleife eintritt. Die Thread-Klasse bietet für solche Fälle mit der Methode Sleep eine bessere Alternative, einen Thread für eine bestimmte Zeitdauer anzuhalten und damit die Ausführung zu verzögern.
Beachten Sie, dass diese Methoden statisch definiert sind und nicht auf eine bestimmte Thread-Instanz aufgerufen werden können, sondern nur aus dem Code des aktuell laufenden Threads heraus, der sich damit selbst aus dem Verkehr zieht. Im Gegensatz zu den im ersten Beispiel verwendeten Schleifen zur Simulation einer länger andauernden Thread-Operation ist Sleep unabhängig von der Taktfrequenz des Computers. Nur der Thread selbst kann bestimmen, ob er sich selbst zur Ruhe legt oder nicht, ein anderer Thread hat keinen Einfluss darauf. Sie rufen Sleep auf, indem Sie entweder die Anzahl der Millisekunden angeben, die sich der Thread zurückziehen soll, oder Sie übergeben eine Referenz auf ein TimeSpan-Objekt. Den Effekt, den wir mit Thread.Sleep erzielen können, wollen wir uns an einem Beispiel ansehen.
Auch in diesem Beispiel wird eine Methode MyProcedure in einem zweiten Thread ausgeführt. Der Primär-Thread wird 20 ms stillgelegt, der Sekundär-Thread für jeweils nur 5 ms pro Schleifendurchlauf. Damit erhält der Sekundär-Thread circa viermal so viel Prozessorzeit wie der Primär-Thread, was sich auch an der Konsole zeigt (siehe Abbildung 11.3).
Abbildung 11.3 Konsolenausgabe des Beispiels »SleepingThread« Bisher wissen Sie, dass Sleep eine Zeitspanne in Form eines Integers oder einer Referenz auf ein TimeSpan-Objekt übergeben wird, um die Ruhezeit des Threads zu beschreiben. Es gibt noch zwei weitere Möglichkeiten, Sleep einzusetzen, was ein bisher noch nicht erörtertes Verhalten aufzwingt:
Wird Sleep die Zahl 0 übergeben, wird der Thread dazu veranlasst, auf den verbleibenden Rest seiner Ausführungszeit zu verzichten und die CPU für den nächsten anstehenden Thread frei zu machen. Er reiht sich danach sofort wieder in die Warteschlange ein. Wartende ThreadsEin Thread, der sich auf eine unbestimmte Zeit zur Ruhe begibt, ruft die Sleep-Methode mit dem Wert Timeout.Infinite auf, also:
Die Klasse Timeout ist wohl so mager ausgestattet wie kaum eine andere des .NET Frameworks. Sie enthält nur die Konstante Infinite, die den Wert –1 repräsentiert. Ein auf diese Weise eingefrorener Thread kommt nicht mehr automatisch zum Zuge, wenn es darum geht, vom Scheduler ein Stück Zeitscheibe zu erhaschen, denn er ist nicht bereit, sondern er wartet. Dieser Zustand kann nur durch einen anderen Thread aufgehoben werden. Dazu ruft der aktive Thread die Methode Interrupt auf den wartenden Thread auf:
Die Sleep-Methode ist also leistungsfähiger, als es im ersten Augenblick den Anschein hat. Fassen wir die möglichen Argumente noch einmal kurz in tabellarischer Form zusammen:
Einem anderen Thread die Zeitscheibe entziehenMit der Methode Sleep kann sich ein Thread selbst einfrieren, auf einen anderen Thread kann diese Methode nicht aufgerufen werden. Soll ein Thread einem anderen die Zeitscheibe entziehen, muss er die Methode Suspend auf den entsprechenden Thread aufrufen:
Ein Thread kann diese Methode auch auf sich selbst aufrufen. Das kommt dann der Übergabe der Konstanten Timeout.Infinite an Sleep gleich. Der Aufruf von Suspend auf einen Thread, der seine Ausführung beendet hat oder noch nicht gestartet worden ist, führt zu der Ausnahme ThreadStateException. Besteht die Gefahr, auf einen solchen Thread zu treffen, muss der Zustand des betreffenden Threads zuvor ermittelt werden. Dazu greift man auf ein Member der Enumeration ThreadState zurück.
Die Member der Enumeration ThreadState werden bitweise kombiniert. Wollen Sie sicherstellen, dass beim Aufruf von Suspend keine Ausnahme ausgelöst wird, müssen Sie daher, wie nachfolgend gezeigt, den Zustand des Threads abfragen:
Ein Thread, dem mit Suspend die Zeitscheibe entzogen wurde, muss wieder angestoßen werden, bevor er seine ihm zugedachte Aufgabe weiter bearbeiten kann. Dazu wird die Methode Resume auf die Referenz des wartenden Threads aufgerufen. Auf einen Thread darf mehrfach Suspend ausgeführt werden, ohne dass es zu einer Fehlermeldung kommt. Allerdings reicht ein einziges Resume, um den Thread wieder in die Warteschlange auf die Zeitscheibe einzureihen. Sicheres Beenden eines ThreadsEinem Thread auf eine bestimmte oder unbestimmte Zeit die Zeitscheibe zu entziehen, ist eine Sache. Eine andere ist das Terminieren eines Threads mit der Methode Thread.Abort. Der Aufruf bewirkt in der Laufzeitschicht die Auslösung der Ausnahme ThreadAbortException. Damit ist es möglich, die Methode ordnungsgemäß zu beenden, beispielsweise um dabei offene Ressourcen zu schließen. Dazu zunächst ein Beispiel. Diesmal wird die Routine, die in einem zweiten Thread ausgeführt wird, in einer eigenen Klasse definiert. Damit ändert sich grundsätzlich nichts, da dem Delegaten nun die Adresse der Instanzmethode in der Klasse mitgeteilt wird.
Sehen wir uns zuerst den Programmcode an. Nach dem Instanziieren der Thread-Klasse wird der zweite Thread gestartet. Da wir die Abort-Methode testen wollen, müssen wir dafür sorgen, dass Abort nicht auf einen Thread trifft, der nicht mehr ausgeführt wird. Deshalb ist in ThreadExecution der Klasse ClassA eine Schleife eingebaut, die eine längere Zeit für einen vollständigen Durchlauf benötigt. Die Zeit muss so groß angesetzt werden, dass Abort auf die sich noch in Arbeit befindliche Schleife trifft. Vor dem Aufruf von Abort wird der Primär-Thread zunächst mit Sleep gebremst, damit der Sekundär-Thread etwas Zeit zu arbeiten hat. Nach dem Aufruf von Abort bekommt das System mit einem zweiten Sleep-Aufruf noch Zeit, den Sekundär-Thread endgültig zu beenden. Durch Auswertung der Eigenschaft IsAlive auf dem Sekundär-Thread wird festgestellt, ob dieser noch aktiv ist oder nicht. Würden wir dem Haupt-Thread keine Ruhepause gönnen, könnte eine falsche Aussage die Folge sein, da die If-Bedingungsprüfung vor der Aufgabe des Sekundär-Threads durchgeführt wird, weil sich Abort und If innerhalb derselben Zeitscheibe befinden und der freigegebene Thread noch keine Möglichkeit erhalten hat, die Ausnahme auszulösen. Die zweite Schleife in der Methode ThreadExecution der Klasse ClassA soll ebenfalls eine länger andauernde Operation simulieren. An der Konsole erfolgt die folgende Ausgabe:
Festzustellen ist ein anscheinender Widerspruch zu der Aussage in Kapitel 9, dass die hinter Finally stehenden Anweisungen ausgeführt werden: Der Aufruf von Abort löst die Exception ThreadAbortException aus, aber die zweite Schleife im Sekundär-Thread wird nicht mehr durchlaufen. Genau in diesem Punkt liegt das Besondere dieser Ausnahme, denn sie wird ausgelöst und auch gefangen, aber die Anweisungen hinter dem Ende der Ausnahmebehandlung kommen nicht mehr zur Ausführung, da der Thread in diesem Moment bereits terminiert ist. Allerdings unterstützt die Laufzeitschicht abschließende Anweisungen in Finally. Gegen das außerplanmäßige Beenden kann sich der betroffene Thread allerdings auch zur Wehr setzen. Dazu muss im Catch-Block des Exceptionhandlers die statische Methode ResetAbort aufgerufen werden:
Bauen Sie diese Anweisung in den Programmcode des Beispiels ein, wird auch die zweite Schleife in ThreadExecution ausgeführt und die bedingte Prüfung mit If führt zu dem Ergebnis, dass der Thread noch lebt – das allerdings auch nur, weil die zweite Schleife ebenfalls wieder eine längere Zeit in Anspruch nimmt oder der Thread nicht schon auf normalem Wege aufgegeben worden ist, bevor die Prüfung erfolgt. Abhängige Threads – die Methode »Join«Nun wäre die folgende Ausgangssituation vorstellbar: Der Primärthread beendet den Sekundär-Thread mit Abort und muss dabei sicherstellen, dass die Anweisungen im Sekundär-Thread zuerst vollständig abgearbeitet sind, bevor die nächste Anweisung im Primär-Thread ausgeführt wird. Solche Situationen können auftreten, wenn der Code des Primär-Threads auf das ordnungsgemäße Beenden angewiesen ist. Das heißt aber auch, dass der Aufruf synchron erfolgen muss, also auf die quasi gleichzeitige Ausführung, die ansonsten die Threads auszeichnet, bewusst verzichtet wird. Wir wollen, um uns der Problematik bewusst zu werden, zunächst eine kleine Änderung in Main vornehmen. Die Implementierung der Klasse ClassA bleibt wie im Beispiel AbortThread erhalten (also ohne den Aufruf von ResetAbort, falls Sie damit experimentiert haben sollten).
Main enthält eine Anweisung, die nach dem Aufruf der Abort-Methode die Konsolenausgabe
erzwingt. Damit sollen Anweisungen simuliert werden, die auf das ordnungsgemäße Terminieren des sekundären Threads angewiesen sind. Sehen wir uns zunächst die Konsolenausgabe des Programmcodes in Abbildung 11.4 an.
Abbildung 11.4 Abhängige Threads – unerwünschter Programmfluss Deutlich ist zu erkennen, dass der sekundäre Thread nach Abort immer noch aktiv ist – die Catch- und Finally-Blöcke werden nach der abhängigen Anweisung ausgeführt. Jetzt hilft eine andere Methode der Klasse Thread weiter: Join, die den aktuellen, also aufrufenden Thread so lange blockiert, bis der Sekundär-Thread vollständig terminiert ist. Sinnvollerweise wird Join direkt hinter Abort aufgerufen. Der Programmablauf kehrt erst dann zum Aufrufer zurück, wenn die Thread-Ausführung ordnungsgemäß beendet ist.
Abbildung 11.5 Ausgabe nach dem sicheren Beenden des Threads Vergleichen wir diese Ausgabe mit der, die wir ohne Join hatten (Abbildung 11.4), können wir eindeutig erkennen, dass der Thread, dessen Terminierung angestoßen wurde, zuerst vollständig abgearbeitet wird, bevor der Aufrufer seinen eigenen Programmfluss fortsetzt. Thread-PrioritätenJeder Thread hat eine Priorität. Mit der Eigenschaft Priority lässt sich die Priorität eines Threads erhöhen, verringern oder einfach nur auswerten. Die Priorität spielt eine entscheidende Rolle bei der Vergabe der Zeitscheibe: Ein Thread hat Vorrang vor einem anderen Thread mit niedrigerer Priorität – vorausgesetzt natürlich, dass sich beide durch den Zustand bereit beschreiben lassen. Priority ist vom Typ der Enumeration ThreadPriority, die fünf Member definiert:
Die Prioritäten können von der höchsten Stufe (Threadpriority.Highest) bis zur niedrigsten (ThreadPriority.Lowest) eingestellt werden. Die automatisch einem Thread zugewiesene Priorität lautet Normal. Der Thread mit der höchsten Priorität erhält die Zeitscheibe und läuft so lange, bis
Am häufigsten ist der Fall anzutreffen, dass sich mehrere Threads gleicher Priorität in die Warteschlange zur CPU eingeordnet haben. Alle erhalten gleiche Zeitanteile nach einem Verfahren, das als Round Robin-Verteilungsverfahren bezeichnet wird. Dabei wird karussellartig die Zeitscheibe auf die bereiten Threads verteilt. Im folgenden Beispielprogramm wollen wir die Auswirkungen der Prioritätsfestlegung in einer Anwendung studieren.
Um den Unterschied deutlich zu machen, empfiehlt es sich, beim ersten Versuch die Anweisung zur Erhöhung der Priorität des ersten Threads auszukommentieren. Starten Sie mit dieser Vorgabe die Laufzeit, werden Sie eine Konsolenausgabe wie in Abbildung 11.6 gezeigt erhalten. thread1 wird gestartet, schreibt ein paar Punkte in die Ausgabe und übergibt danach dem Prozessor den thread2, der sich durch eine eigene Zeichenfolge bemerkbar macht. Die Zeitscheibe dauert lang genug, um die Anweisungen von thread2 vollständig zu bearbeiten. Danach übernimmt wieder thread1 die CPU und beendet seine Ausführung.
Abbildung 11.6 Konsolenausgabe ohne Hochsetzen der Priorität Erhöhen wir nun die Priorität des ersten Threads um eine Stufe und starten ein zweites Mal die Laufzeit (siehe Abbildung 11.7). Da thread1 nun die höchste Priorität aller sich in der Warteschlange befindlichen Threads hat (besser gesagt, zumindest die Priorität im Vergleich zu thread2 ist höher), werden seine Anweisungen zuerst vollständig ausgeführt, bevor thread2 mit seiner geringeren Priorität an die Reihe kommt. Einem Thread eine gewisse Sonderstellung durch die Erhöhung der Priorität einzuräumen, mag vielleicht manchmal ganz verlockend klingen. Bedenken Sie jedoch, dass dieser Thread bei einer lang andauernden Operation eine bremsende Wirkung auf die anderen Threads hat. Man spricht auch von einem Aushungern des Systems. Gehen Sie daher sorgfältig mit dem Erhöhen von Prioritäten um, und achten Sie darauf, dass keine unnötigen Operationen von einem solchen Thread ausgeführt werden, sondern nur solche, die für den weiteren Ablauf der Anwendung unbedingt notwendig sind.
Abbildung 11.7 Konsolenausgabe nach dem Hochsetzen der Priorität Vorder- und Hintergrund-ThreadsThreads werden in zwei Kategorien unterteilt: in Vorder- und in Hintergrund-Threads. Ein Prozess wird ausgeführt, solange noch mindestens ein Vordergrund-Thread existiert. Mit dem Beenden des letzten Vordergrund-Threads wird der Prozess der Anwendung selbst dann beendet, wenn Hintergrund-Threads noch aktiv sind und die ihnen auferlegte Aufgabe noch nicht vollständig ausgeführt haben. Die Eigenschaft IsBackground beschreibt, ob ein Thread als Vorder- oder Hintergrund-Thread eingestuft ist. Grundsätzlich sind alle Threads, die aus der Klasse Thread erzeugt werden, zunächst Vordergrund-Threads. Mit IsBackground lässt sich ein Thread aber auch zu einem Hintergrund-Thread degradieren. Im folgenden Beispielprogramm ist der Effekt des Unterschieds zwischen einem Vorder- und Hintergrund-Thread deutlich zu erkennen. In Main wird ein neuer Thread erzeugt und als Hintergrund-Thread festgelegt. Main läuft selbst in einem Vordergrund-Thread, der terminiert, bevor der Hintergrund-Thread seine Aufgabe vollständig ausgeführt hat – die Ausgabe des Hintergrund-Threads an der Konsole ist unvollständig. Kommentieren Sie die Anweisung aus, in welcher der zweite Thread zum Hintergrund-Thread wird, wird der Prozess erst in dem Moment beendet, wenn beide Threads die ihnen zugestandene Aufgabe abgeschlossen haben.
11.2.2 Threadpools nutzen
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\ Kapitel 11\ThreadpoolDemo |
| ' ---------------------------------------------------------- |
| Imports System.Threading |
| Module Module1 |
| Sub Main() |
| ' den Threadpool erforschen |
| Dim maxThreads As Integer |
| Dim asyncThreads As Integer |
| ThreadPool.GetMaxThreads(maxThreads, asyncThreads) |
| Console.WriteLine("Max. Anzahl Threads: {0}", maxThreads) |
| Console.WriteLine("Max. Anzahl E/A-Threads: {0}", asyncThreads) |
| Console.WriteLine() |
| ' Benachrichtigungsereignis, Zustand 'nicht signalisieren' |
| Dim ready As AutoResetEvent = New AutoResetEvent(False) |
| ' Anfordern eines Threads aus dem Pool |
| ThreadPool.QueueUserWorkItem( |
| New WaitCallback(AddressOf Calculate), ready) |
| Console.WriteLine("Der Haupt-Thread wartet ...") |
| ' Haupt-Thread in den Wartezustand setzen |
| ready.WaitOne() |
| Console.WriteLine("Sekundär-Thread ist fertig.") |
| Console.ReadLine() |
| End Sub |
| Public Sub Calculate(ByVal obj As Object) |
| Console.WriteLine("Im Sekundär-Thread") |
| Thread.Sleep(5000) |
| ' Ereigniszustand auf 'signalisieren' festlegen |
| obj.Set() |
| End Sub |
| End Module |
Die Methode Calculate soll in einem Thread aus dem Threadpool ausgeführt werden. Bevor diese Operation eingeleitet wird, wollen wir aber noch feststellen, wie viele Threads uns der Pool zur Verfügung stellt, und rufen die statische Methode GetMaxThreads auf. Über dem ersten Parameter werden uns die Threads geliefert, der zweite Parameter gibt darüber hinaus Auskunft über die maximale Anzahl der möglichen E/A-Anforderungen. Sie werden feststellen, dass sich 25 Threads im Pool befinden, und zwar pro Prozessor.
Das Beispiel ist so entwickelt, dass nicht nur ein Thread aus dem Pool zur Ausführung der Methode Calculate herangezogen wird. Darüber hinaus wird auch ein Synchronisationsszenario in Gang gesetzt, das bewirkt, dass während der Ausführung von Calculate der aufrufende Code in Wartestellung versetzt wird und auf ein Signal von Calculate wartet, bevor er seine Arbeit wieder aufnimmt. Mehr zur Synchronisierung erfahren Sie im folgenden Abschnitt.
Dem Aufruf der statischen Methode QueueUserWorkItem wird ein Delegat, der die im Thread auszuführende Methode beschreibt, übergeben. Darüber hinaus kann QueueUserWorkItem ein zweites Argument übergeben werden, um der Thread-Methode Daten bereitzustellen. Hier wird dem zweiten Parameter ein Objekt vom Typ AutoResetEvent übergeben. Dieses Objekt versetzt zwei Threads in die Lage, über Signale miteinander zu kommunizieren. Erzeugt wird das Objekt im Code mit:
| Dim ready As AutoResetEvent = New AutoResetEvent(False) |
Der Übergabeparameter False besagt, dass der anfängliche Zustand des Objekts auf »nicht signalisiert« festgelegt wird. Mit
| ready.WaitOne() |
wird der aktuelle Thread so lange blockiert, bis er ein Signal erhält. Dieses stammt aus der Thread-Methode und wird durch Aufruf der Set-Methode des AutoResetEvent-Objekts ausgelöst:
| obj.Set() |
Hier profitieren wir davon, der Thread-Methode im zweiten Parameter die Referenz auf das AutoResetEvent übergeben zu haben.
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2007
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken.
Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die
gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich
geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung,
Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.